package gestionedt.solveur;

import static choco.Choco.*;
import choco.cp.model.*;
import choco.cp.solver.*;
import choco.kernel.model.Model;
import choco.kernel.model.constraints.*;
import choco.kernel.model.variables.integer.*;
import choco.kernel.solver.Solver;

import gestionedt.controllers.PrestationJpaController;
import gestionedt.controllers.exceptions.NonexistentEntityException;
import gestionedt.models.*;
import java.util.List;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Implémentation de la classe abstraite SolveurEDT en utilisant CHOCO.
 */
public class SolveurEDTCHOCO extends SolveurEDT {

    /*MODELE*/
    private Model m;

    /*SOLVEUR*/
    private Solver s;

    /** VARIABLES DE TRAVAIL **/
    /**
     * Tableau à deux dimensions de variables booléennes tel que Q[i][k] indique
     * si la prestation i a lieu au créneau k ou non.
     */
    private IntegerVariable[][] Q;
    /**
     * Tableau à deux dimensions de variables booléennes tel que U[i][j] indique
     * si la prestation i est suivie par le groupe j ou non.
     */
    private IntegerVariable[][] U;
    /**
     * Tableau à trois dimensions de variables booléennes tel que
     * T[i][j][k] = U[i][j] &cap; Q[i][k] (T[i][j][k] indique donc si la
     * prestation i a lieu au créneau k et est suivie par le groupe j ou non).
     */
    private IntegerVariable[][][] T;

    /**
     * Constructeur.
     * @param enseignants
     * @param modules liste des modules (si elle comporte des modules terminés, ils seront retirés)
     * @param pools
     * @param prestations liste des prestations (si elle comporte des prestations de modules terminés, elles seront retirées)
     * @param groupes
     * @param creneaux
     */
    public SolveurEDTCHOCO(List<Enseignant> enseignants,
                           List<Module> modules,
                           List<Pool> pools,
                           List<Prestation> prestations,
                           List<Groupe> groupes,
                           List<Creneau> creneaux) {

        super(enseignants, modules, pools, prestations, groupes, creneaux);
    }

    /**
     * Initialise le modèle, c'est-à-dire instanciation du modèle
     * puis création des variables.
     */
    private void initialiserModele() {
        /**
         * instanciation du modèle
         */
        this.m = new CPModel();
        /**
         * initialisation des variables
         */
        Q = new IntegerVariable[prestations.size()][creneaux.size()];
        for(int i=0; i<prestations.size(); i++)
            for(int k=0; k<creneaux.size(); k++)
                Q[i][k] = makeIntVar("Q"+i+"_"+k, 0, 1);

        U = new IntegerVariable[prestations.size()][groupes.size()];
        for(int i=0; i<prestations.size(); i++)
            for(int j=0; j<groupes.size(); j++)
                U[i][j] = makeIntVar("U"+i+"_"+j, 0, 1);

        T = new IntegerVariable[prestations.size()][groupes.size()][creneaux.size()];
        for(int i=0; i<prestations.size(); i++)
            for(int j=0; j<groupes.size(); j++)
                for(int k=0; k<creneaux.size(); k++)
                    T[i][j][k] = makeIntVar("T"+i+"_"+j+"_"+k, 0, 1);
    }

    /**
     * Mise en place des contraintes en fonction des données fournies en entrée.
     */
    private void definirContraintes() {

        Constraint c; // variable intermédiaire pour ajouter une contrainte au modèle

        // Lien entre les variables T et les variables Q et U :
        for(int i=0; i<prestations.size(); i++)
            for(int j=0; j<groupes.size(); j++)
                for(int k=0; k<creneaux.size(); k++)
                    m.addConstraint(times(U[i][j], Q[i][k], T[i][j][k]));

        // Contrainte 1 : à toute prestation est associé un créneau
        for(int i=0; i<prestations.size(); i++) {
            c = eq(sum(Q[i]), 1);
            m.addConstraint(c);
        }

        // Contrainte 2 : tout groupe doit suivre exactement une prestation
        // de chacun des modules auquel il est inscrit
        for(int j=0; j<groupes.size(); j++) {
            Groupe groupe = groupes.get(j);
            for(Module module : groupe.getModules()) {
                // les modules terminés ne sont pas pris en compte
                if( ! module.getEstTermine()) {
                    Collection<Prestation> prest = module.getPrestations();
                    if(prest.size() > 0) {
                        IntegerVariable[] E = new IntegerVariable[prest.size()];
                        int i = 0;
                        for(Prestation p : prest)
                            E[i++] = U[prestations.indexOf(p)][j];
                        c = eq(sum(E), 1);
                        m.addConstraint(c);
                    }
                }
            }
        }

        // Contrainte 3 : la somme des effectifs des groupes qui suivent une même
        // prestation ne doit pas dépasser l'effectif maximal du module concerné
        for(int i=0; i<prestations.size(); i++) {
            int[] effectifs = new int[groupes.size()];
            for(int j=0; j<groupes.size(); j++)
                effectifs[j] = groupes.get(j).getEffectif();
            int effectifMax = prestations.get(i).getModule().getEffectifMax();
            c = leq(scalar(effectifs, U[i]), effectifMax);
            m.addConstraint(c);
        }

        // Contrainte 4 : deux prestations suivies par un même groupe
        // d'étudiants ne peuvent avoir lieu en même temps
        for(int j=0; j<groupes.size(); j++) {
            for(int k=0; k<creneaux.size(); k++) {
                c = leq(sum3(T, j, k), 1);
                m.addConstraint(c);
            }
        }

        // Contrainte 5 : deux prestations dispensées par un même
        // enseignant ne peuvent avoir lieu simultanément
        for(int k=0; k<creneaux.size(); k++) {
            for(int e=0; e<enseignants.size(); e++) {
                Enseignant ens = enseignants.get(e);
                Collection<Prestation> prest = new LinkedList<Prestation>();
                for(Prestation p : ens.getPrestationsAssurees())
                    if( ! p.getModule().getEstTermine())
                        prest.add(p); // seules les prestations de modules non terminés sont gardées
                if(prest.size() > 0) {
                    IntegerVariable[] E = new IntegerVariable[prest.size()];
                    int i = 0;
                    for(Prestation p : prest)
                        E[i++] = Q[prestations.indexOf(p)][k];
                    c = leq(sum(E), 1);
                    m.addConstraint(c);
                }
            }
        }

        // Contrainte 6 : une prestation ne peut avoir lieu à un horaire
        // que si l'enseignant auquel elle est assignée est disponible
        for(int i=0; i<prestations.size(); i++) {
            Enseignant ens = prestations.get(i).getEnseignant();
            Collection<Creneau> indisponibilites = ens.getIndisponibilites();
            if(indisponibilites.size() > 0) {
                IntegerVariable[] I = new IntegerVariable[indisponibilites.size()];
                int k = 0;
                for(Creneau indisponibilite : indisponibilites)
                    I[k++] = Q[i][creneaux.indexOf(indisponibilite)];
                c = eq(sum(I), 0);
                m.addConstraint(c);
            }
        }

        // Contrainte 7 : une prestation ne peut avoir lieu à un horaire
        // que si les groupes qui la suivent sont disponibles
        for(int j=0; j<groupes.size(); j++) {
            Collection<Creneau> indisponibilites = groupes.get(j).getIndisponibilites();
            for(Creneau indisponibilite : indisponibilites) {
                int k = creneaux.indexOf(indisponibilite);
                c = eq(sum3(T, j, k), 0);
                m.addConstraint(c);
            }
        }

        // Contrainte 8 : Le nombre de prestations, ayant lieu simultanément,
        // de modules appartenant à un même pool ne doit pas dépasser
        // le nombre autorisé pour ce pool
        for(int k=0; k<creneaux.size(); k++) {
            for(Pool pool : pools) {
                int nbPrestMax = pool.getNbPrestMaxParPlage();
                List<IntegerVariable> P = new ArrayList<IntegerVariable>();
                for(Module mod : pool.getModules()) {
                    if( ! mod.getEstTermine()) {
                        for(Prestation prest : mod.getPrestations()) {
                            int i = prestations.indexOf(prest);
                            P.add(Q[i][k]);
                        }
                    }
                }
                // la contrainte est ajoutée seulement si le nombre de prestations,
                // ayant lieu durant le créneau k et dont le module non terminé est
                // associé au pool courant, est supérieur à 0
                if(P.size() > 0) {
                    c = leq(sum(P.toArray(new IntegerVariable[0])), nbPrestMax);
                    m.addConstraint(c);
                }
            }
        }

        // affectations de groupes et de créneaux aux prestations déjà fixées,
        // autrement dit le champ de recherche est par conséquent restreint
        for(int i=0; i<prestations.size(); i++) {
            Prestation prestation = prestations.get(i);
            // affections de groupes
            for(Groupe groupe : prestation.getGroupes()) {
                int j = groupes.indexOf(groupe);
                c = eq(U[i][j], 1);
                m.addConstraint(c);
            }
            if(prestation.getCreneau() != null) {
                // affection d'un créneau
                int k = creneaux.indexOf(prestation.getCreneau());
                c = eq(Q[i][k], 1);
                m.addConstraint(c);
            }
        }
    }

    @Override
    public boolean resoudre() {
        // initialisation du modèle (création des variables...)
        initialiserModele();
        // définition des contraintes pour le modèle
        definirContraintes();

        // instanciation du solveur
        s = new CPSolver();
        s.read(m); // le modèle est fourni au solveur
        boolean trouvee = s.solve(); // lancement de la résolution
        if(trouvee) { // si une solution a été trouvée
            creerLiensAvecRes(); // les objets sont mis à jour en fonction de la solution
            enregistrerResultat(); // puis la persistance du résultat est réalisée
        }
        return trouvee;
    }

    /**
     * Création des liens entre les prestations et les groupes, ainsi qu'entre les
     * prestations et les créneaux en fonction du résultat trouvé par le solveur.
     */
    @SuppressWarnings("empty-statement")
    private void creerLiensAvecRes() {
        for(Prestation prestation : prestations) {
            int i = prestations.indexOf(prestation);

            // création des liens entre les prestations et les groupes
            Collection<Groupe> groupesInscrits = prestation.getGroupes();
            for(int j=0; j<groupes.size(); j++) {
                // récupération de la valeur booleénne qui indique
                // si le groupe j doit suivre la prestation i
                boolean doitSuivre = s.getVar(U[i][j]).getVal() == 1;
                if(doitSuivre) {
                    // ajout du lien (seulement s'il n'existe pas encore)
                    Groupe groupe = groupes.get(j);
                    if( ! groupesInscrits.contains(groupe))
                        groupesInscrits.add(groupe);
                }
            }

            // affection d'un créneau pour chaque prestation
            int k = 0; // parcours du tableau Q[i] jusqu'à tomber sur 1
            while(s.getVar(Q[i][k++]).getVal() == 0);
            prestation.setCreneau(creneaux.get(--k));
        }
    }

    /**
     * Enregistrement du résultat (persistance des liens établis).
     */
    private void enregistrerResultat() {
        PrestationJpaController controller = new PrestationJpaController();
        try {
            for(Prestation prestation : prestations) {
                controller.edit(prestation);
            }
        } catch (NonexistentEntityException ex) {
            Logger.getLogger(SolveurEDTCHOCO.class.getName()).log(Level.SEVERE, null, ex);
        } catch (Exception ex) {
            Logger.getLogger(SolveurEDTCHOCO.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * Retourne une variable entière qui correspond à la somme des variables
     * d'un tableau à trois dimensions telle que :
     * T[1][j][k] + T[2][j][k] + ... + T[n][j][k]
     * où les indices j et k sont passés en paramètres.
     * @param arg0 tableau à trois dimensions
     * @param j
     * @param k
     * @return variable entière
     */
    static private IntegerExpressionVariable sum3(IntegerVariable[][][] arg0, int j, int k) {
        IntegerVariable[] tab = new IntegerVariable[arg0.length];
        for(int i=0; i<arg0.length; i++)
            tab[i] = arg0[i][j][k];
        return sum(tab);
    }

}
